<?php

/**
 * 用于实例化应用程序的类
 */
class App
{
    /**
     * 单例模式
     *
     * @var App|null
     */
    private static $instance = null;

    /**
     * 标记run方法是否已经运行
     *
     * @var bool
     */
    private static bool $hasRun = false;

    /**
     * 是否开启调试模式
     *
     * @var bool
     */
    public bool $debug = true;

    /**
     * 防止外部实例化
     */
    private function __construct()
    {
    }

    /**
     * 防止克隆
     */
    private function __clone()
    {
    }

    /**
     * 防止反序列化创建对象
     */
    public function __wakeup()
    {
    }

    /**
     * 框架版本
     *
     * @var string
     */
    const VERSION = '0.0.1';

    /**
     * 默认控制器
     *
     * @var string
     */
    private string $defaultController = 'Home';

    /**
     * 默认方法
     *
     * @var string
     */
    private string $defaultMethod = 'index';

    /**
     * 获取单例实例
     *
     * @return App
     */
    public static function getInstance(): App
    {
        if (self::$instance === null) {
            self::$instance = new self();
        }
        return self::$instance;
    }

    /**
     * 运行应用程序
     *
     * 确保此方法只运行一次。如果尝试多次运行，将抛出异常。
     *
     * @throws \Exception 如果run方法已经运行过
     * @return void
     */
    public function run(): void
    {
        if (self::$hasRun) {
            throw new \Exception('应用程序已经运行过一次，不能再次运行。');
        }

        // 检测 PHP 版本
        if (version_compare(PHP_VERSION, '7.4.0', '<=')) {
            echo 'PHP >= 7.4.0, my version: ' . PHP_VERSION . "\n";
        }

        // 开启 session
        if (session_status() !== PHP_SESSION_ACTIVE) {
            session_start();
        }

        // 报告所有错误
        error_reporting(E_ALL);
        // 对用户隐藏错误
        ini_set('display_errors', '0');

        set_error_handler([$this, 'errorHandler']);
        register_shutdown_function([$this, 'shutdownHandler']);

        try {
            $this->route();
        } catch (\Throwable $th) {
            self::handleException($th);
        }

        // 标记run方法已经运行
        self::$hasRun = true;
    }

    /**
     * 自定义错误处理
     *
     * 该函数用于处理运行时错误，根据调试模式决定是否记录日志或显示错误信息。
     *
     * @param int $code 错误代码
     * @param string $message 错误消息
     * @param string $file 发生错误的文件名
     * @param int $line 发生错误的行号
     * @return void 该函数没有返回值
     */
    private function errorHandler(int $code, string $message, string $file, int $line): void
    {
        $errorDetails = [
            'code'    => $code,
            'message' => $message,
            'file'    => $file,
            'line'    => $line,
            'preview' => '',
        ];
        self::handleError($errorDetails);
    }

    /**
     * 自定义脚本结束时执行的函数
     *
     * 该函数在脚本执行结束时调用，用于检查是否存在最后一个致命错误。
     * 如果存在致命错误，根据调试模式决定是否记录日志或显示错误信息。
     *
     * @return void 该函数没有返回值
     */
    private function shutdownHandler(): void
    {
        $lastError = error_get_last();
        if ($lastError && in_array($lastError['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR])) {
            $errorDetails = [
                'code'    => $lastError['type'],
                'message' => $lastError['message'],
                'file'    => $lastError['file'],
                'line'    => $lastError['line'],
                'preview' => '',
            ];
            self::handleError($errorDetails);
        }
    }

    private static function handleError(array $errorDetails): void
    {
        // if ($this->debug === false) {
        //     self::logError($errorDetails['message'], $errorDetails['file'], $errorDetails['line']);
        //     exit(1); // 使用非零退出码表示错误
        // }

        self::generateErrorPreview($errorDetails);
        self::handleAndDisplayError($errorDetails);
    }

    private function handleException(Throwable $e): void
    {
        $errorDetails = [
            'code'    => $e->getCode(),
            'message' => $e->getMessage(),
            'file'    => $e->getFile(),
            'line'    => $e->getLine(),
            'preview' => '',
        ];

        if ($this->debug === false) {
            self::logError($errorDetails['message'], $errorDetails['file'], $errorDetails['line']);
            exit(1);
        }
        self::generateErrorPreview($errorDetails);
        self::handleAndDisplayError($errorDetails);
    }

    /**
     * 生成友好的错误代码预览
     *
     * 该函数会读取错误发生的文件并生成一个包含错误代码上下文的HTML预览。
     * 预览会在错误代码行高亮显示，并包含行号。
     *
     * @param array $errorDetails 错误详细信息的数组，包含文件名和行号等信息
     * @param int $offset 错误行上下文的行数偏移量（默认值为5）
     * @return void 该函数没有返回值，但会修改传入的错误详细信息数组
     */
    private static function generateErrorPreview(array &$errorDetails, int $offset = 5): void
    {
        $file = $errorDetails['file'];
        $line = $errorDetails['line'];

        // 检查文件是否存在且可读
        if (file_exists($file) && is_readable($file)) {
            $fileContents = file($file);
            $start        = max(0, $line - $offset);
            $end          = min(count($fileContents), $line + $offset);

            $preview = '';
            for ($i = $start; $i < $end; $i++) {
                $text = trim($fileContents[$i], "\n\r\0\x0B");
                $text = str_replace(["    ", "  "], "\t", htmlspecialchars($text, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'));
                $text = str_replace("\t", "<span class=\"tab\"></span>", $text);

                // 格式化行号
                $index = sprintf("%02d", $i + 1);

                // 高亮显示错误行
                if ($i == $line - 1) {
                    $preview .= sprintf('<p class="line" id="target"><span class="index">%s</span><span class="e-message">%s</span></p>', $index, $text);
                } else {
                    $preview .= sprintf('<p class="line"><span class="index">%s</span>%s</p>', $index, $text);
                }
            }

            // 将预览HTML添加到错误详细信息数组中
            $errorDetails['preview'] = $preview;
        }
    }

    /**
     * 处理和显示错误
     *
     * 该函数将错误详细信息编码为JSON格式，加载错误模板并输出错误信息。
     *
     * @param array $errorDetails 错误详细信息的数组，包含错误代码、消息、文件名、行号等信息
     * @return void 该函数没有返回值
     */
    private static function handleAndDisplayError(array $errorDetails): void
    {
        // 将错误详细信息编码为JSON格式
        $jsonError = json_encode($errorDetails, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);

        ob_start();
        include_once __DIR__ . '/template/error.php';
        $output = ob_get_clean();

        echo $output;
        exit(1);
    }

    /**
     * 记录错误日志
     *
     * 该函数创建一个日志记录器实例，并记录给定的错误消息、文件名和行号。
     * 如果日志记录失败，会捕获异常并输出错误信息。
     *
     * @param string $message 错误消息
     * @param string $file 发生错误的文件名
     * @param int $line 发生错误的行号
     * @return void 该函数没有返回值
     */
    private static function logError(string $message, string $file, int $line): void
    {
        try {
            $log = new Logger();
            $log->debug("\n\t" . $message . "\n\t\t#{$line} " . $file .  PHP_EOL);
            unset($log);
        } catch (Exception $e) {
            // 日志记录失败，可以考虑输出到标准错误输出或其他备用日志机制
            echo "Error logging failed: " . $e->getMessage();
        }
    }

    /**
     * 解析网址并路由请求
     *
     * @return void
     */
    public function route(): void
    {
        // 获取当前路径并去除前后斜杠
        $url = ltrim(parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH), '/');
        // 清理路径
        $url = filter_var($url, FILTER_SANITIZE_URL);

        // 分割路径以获取控制器和方法
        $segments = explode('/', $url);

        // 使用正则表达式验证控制器和方法名称
        $controllerName = !empty($segments[0]) && preg_match('/^[a-zA-Z0-9]+$/', $segments[0]) ? $segments[0] : $this->defaultController;
        $methodName = !empty($segments[1]) && preg_match('/^[a-zA-Z0-9]+$/', $segments[1]) ? $segments[1] : $this->defaultMethod;

        // 构建控制器文件名
        $controllerFile = __DIR__ . '/../app/controllers/' . ucfirst($controllerName) . '.php';

        // 加载控制器文件
        if (is_file($controllerFile)) {
            require_once $controllerFile;
        } else {
            die( "Controller file for $controllerName not found.");
            // $this->handleError(404, "Controller file for $controllerName not found.");
            // return;
        }

        // 获取方法参数，从路径的其余部分获取（假设它们是方法的参数）
        $params = array_slice($segments, 2);

        // 构建控制器类的名称
        $controllerClass = 'app\\controllers\\' . ucfirst($controllerName);

        try {
            // 检查控制器类是否存在
            if (!class_exists($controllerClass)) {
                throw new Exception("Controller class $controllerClass not found.");
            }

            $controller = new $controllerClass();

            // 检查方法是否存在
            if (!method_exists($controller, $methodName)) {
                throw new Exception("Method $methodName not found in controller $controllerClass.");
            }

            // 使用反射来处理方法参数
            $reflection = new ReflectionMethod($controller, $methodName);
            $expectedParams = $reflection->getNumberOfParameters();

            // 检查提供的参数数量是否匹配
            if (count($params) < $reflection->getNumberOfRequiredParameters() || count($params) > $expectedParams) {
                throw new Exception("Incorrect number of parameters for method $methodName in controller $controllerClass.");
            }

            // 调用方法并传递参数
            $reflection->invokeArgs($controller, $params);
        } catch (Exception $e) {
            die('404');
            // $this->handleError(404, $e->getMessage());
        }
    }

    public function setDefaultController(string $value)
    {
        $this->defaultController = $value;
    }

    /**
     * 生成 CSRF token
     *
     * @param int $length token 长度，默认 32 字节
     * @return string 生成的 CSRF token
     */
    public static function csrf_token(int $length = 32): string
    {
        // 如果 session 中已经存在 csrf_token，直接返回它
        if (isset($_SESSION['csrf_token'])) {
            return $_SESSION['csrf_token'];
        }

        // 生成新的 CSRF token
        $token = bin2hex(random_bytes($length));

        // 将 CSRF token 存入 session
        $_SESSION['csrf_token'] = $token;

        return $token;
    }


    /**
     * 调试输出
     *
     * @param mixed ...$value 要输出的值
     */
    public static function dump(...$value): void
    {
        ob_start();
        var_dump(...$value);
        $output = ob_get_clean();
        $output = preg_replace('/]=>\n(\s+)/m', '] => ', $output);
        $output = sprintf('<pre>%s</pre>', htmlspecialchars($output, ENT_SUBSTITUTE));
        echo $output;
    }
}
